React Hooks

0.什么是 React Hooks

Hook 是 React 16.8 的新增特性。它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性。

1.为什么有 React Hooks

https://zh-hans.reactjs.org/docs/hooks-intro.html#motivation

  • 在组件之间复用状态逻辑很难
  • 复杂组件变得难以理解
  • 难以理解的 class

2.怎么用 React Hooks

1.基础

基本用法看官网即可 https://zh-hans.reactjs.org/docs/hooks-reference.html

  • Basic Hooks
    • useState
    • useEffect
    • useContext
  • Additional Hooks
    • useReducer
    • useCallback
    • useMemo
    • useRef
    • useImperativeHandle
    • useLayoutEffect
    • useDebugValue
  • Custom Hooks

2.进阶:

useState:

  1. 陈旧闭包( stale closure https://leewarrick.com/blog/react-use-effect-explained/, https://zh-hans.reactjs.org/docs/hooks-faq.html#why-am-i-seeing-stale-props-or-state-inside-my-function

思考下面的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function Counter() {
const [count, setCount] = useState(0);

function handleAlertClick() {
setTimeout(() => {
alert('You clicked on: ' + count);
}, 3000);
}

useEffect(() => {
console.log(`You clicked ${count} times`);
document.title = `You clicked ${count} times`;
}, []);

return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>Click me</button>
<button onClick={handleAlertClick}>Show alert</button>
</div>
);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function Timer() {
const [count, setCount] = useState(0);
const [randomNum, setRandomNum] = useState(0);

useEffect(() => {
const intervalId = setInterval(() => {
setCount(count + 1);
setRandomNum(Math.random());
}, 1000);
return () => clearInterval(intervalId);
}, []);

return (
<div>
<p>The count is: {count}</p>
<p>RandomNum is {randomNum}</p>
</div>
);
}
  1. 太多的 useState, state 写成对象可不可以:可以,但需自己手动合并更新对象
    https://zh-hans.reactjs.org/docs/hooks-reference.html#functional-updates
1
2
3
4
setState(prevState => {
// 也可以使用 Object.assign
return { ...prevState, ...updatedValues };
});

useReducer 是另一种可选方案,它更适合用于管理包含多个子值的 state 对象。

useEffect:

与生命周期对应

  1. componentDidMount
1
2
3
4
5
componentDidMount() {
fn()
}

useEffect(() => fn(), []);
  1. componentWillUnmount
1
2
3
4
5
componentWillUnmount() {
fn2()
}

useEffect(() => fn2, []);
1
2
3
4
5
6
7
8
9
10
11
12
// 结合看看
componentDidMount() {
fn()
}
componentWillUnmount() {
fn2()
}

useEffect(() => {
fn()
return () => fn2()
}, []);
  1. componentDidUpdate
1
2
3
4
5
6
7
8
const mounting = useRef(true);
useEffect(() => {
if (mounting.current) {
mounting.current = false;
} else {
fn();
}
});

模拟生命周期只是方便初学 hooks 的人快速理解,但想要深入理解,我们不应该用生命周期的方式看待 useEffect(
https://overreacted.io/zh-hans/a-complete-guide-to-useeffect/

image

总结:
https://github.com/dt-fe/weekly/blob/v2/096.%E7%B2%BE%E8%AF%BB%E3%80%8AuseEffect%20%E5%AE%8C%E5%85%A8%E6%8C%87%E5%8D%97%E3%80%8B.md

  1. 每次 Render 都有自己的 Props 与 State
  2. 每次 Render 都有自己的事件处理
  3. 每次 Render 都有自己的 Effects

useLayoutEffect

官网讲了作用,这里给一个实际的例子

useMemo 与 useCallback:

  1. 作用演示
  2. 使用 useMemo 实现 useCallback

useCallback(fn, deps) 相当于 useMemo(() => fn, deps)。

useRef

类似实例变量的东西 https://zh-hans.reactjs.org/docs/hooks-faq.html#is-there-something-like-instance-variables

  1. useRef 解决 stale closure 问题
1
2
3
4
5
6
7
8
9
10
11
12
13
14
function Timer() {
const [count, setCount] = React.useState(0);
const countRef = React.useRef(0);

React.useEffect(() => {
const intervalId = setInterval(() => {
countRef.current = countRef.current + 1;
setCount(countRef.current);
}, 1000);
return () => clearInterval(intervalId);
}, []);

return <div>The count is: {count}</div>;
}
  1. 但不能没有 useState
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// This won’t work:
function Timer() {
const count = React.useRef(0);

React.useEffect(() => {
const intervalId = setInterval(() => {
count.current = count.current + 1;
//console.log('Ref example count: ' + count.current)
}, 1000);
return () => clearInterval(intervalId);
}, []);

return <div>The count is: {count.current}</div>;
}

有了 useReducer 是不是不需要 redux 了?

一定程度上是,但对于我们平时做的项目来说还有很多事要做。

useReducer + useContext + useEffect 才能替代 Redux + Connect + Redux 中间件方案

3.React Hooks 原理

“手写 Hooks”

useState
useEffect
useMemo

  1. “setState”(useState 中的第二个返回值统称)才会更新
  2. 维护一个存储所有 Hooks 的数据结构,一个一个 Hook 处理,处理完递增
  3. 对依赖的存储和比较

手写代码的问题:

  1. 数据结构:数组 vs 链表
  2. render()部分的具体实现:如何与当前 FC 绑定而不是 Render All?获取当前 fiber

源码:
https://github.com/facebook/react/blob/master/packages/react/src/ReactHooks.js

https://github.com/facebook/react/blob/master/packages/react-reconciler/src/ReactFiberHooks.js

Memoization

1
2
3
4
5
6
7
function memoize(fn) {
return function() {
var args = Array.prototype.slice.call(arguments);
fn.cache = fn.cache || {};
return fn.cache[args] ? fn.cache[args] : (fn.cache[args] = fn.apply(this, args));
};
}

4.React Hooks 不足

  1. 暂时不能取代的 API:

getSnapshotBeforeUpdate, getDerivedStateFromError, componentDidCatch

可能会有 useCatch(() => {})

  1. 难维护?
    https://weibo.com/1400854834/I6psH4P6Q?filter=hot&root_comment_id=0&type=comment

6.参考

7.实例

https://www.jianshu.com/p/a47c03eaecba

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// Hook
function useWhyDidYouUpdate(name, props) {
// 获得一个可变的 ref 对象用于存储 props,下次 hook 运行的时候可以比较
const previousProps = useRef();

useEffect(() => {
if (previousProps.current) {
// 获取到之前的和现在的 props 所有 key。
const allKeys = Object.keys({ ...previousProps.current, ...props });
// 使用这个对象追踪 props 的变化。
const changesObj = {};
// 迭代 keys
allKeys.forEach(key => {
if (previousProps.current[key] !== props[key]) {
// 添加到 changesObj
changesObj[key] = {
from: previousProps.current[key],
to: props[key],
};
}
});

// 如果 changesObj 不为空,就输出到 console
if (Object.keys(changesObj).length) {
console.log('[why-did-you-update]', name, changesObj);
}
}

// 最后用当前的 props 更新 previousProps 用于下次 hook 调用
previousProps.current = props;
});
}